import os
import re
import base64
import imghdr
import hashlib
import mimetypes
import subprocess
from urllib.parse import urlparse, unquote
from typing import Tuple, List, Optional

import requests
from loguru import logger

from configs import RESOURCES_DIR


def normalize_filename_extension(filename: str) -> str:
    """Ensure the file extension is lowercase while keeping the basename intact."""
    if not filename:
        return filename
    name, ext = os.path.splitext(filename)
    if not ext:
        return filename
    return f"{name}{ext.lower()}"


def to_abs_path(stored_path: str) -> str:
    """基于 RESOURCES_DIR 将存储路径转换为绝对路径。"""
    if not stored_path:
        return ""
    if os.path.isabs(stored_path):
        return stored_path
    return os.path.join(RESOURCES_DIR, stored_path.lstrip("/\\"))


def to_rel_path(abs_path: str) -> str:
    """将绝对路径转换为相对 RESOURCES_DIR 的路径。"""
    if not abs_path:
        return ""
    try:
        abs_path = os.path.abspath(abs_path)
        base_abs = os.path.abspath(RESOURCES_DIR)
        if abs_path.startswith(base_abs):
            rel = os.path.relpath(abs_path, base_abs)
            return rel.replace('\\', '/')
        return abs_path
    except Exception:
        return abs_path


def cal_md5(file_content):
    file_hash = hashlib.md5()

    file_hash.update(file_content)
    return file_hash.hexdigest()


def write_2_file(file_path, file_content):
    with open(file_path, "wb+") as buffer:
        buffer.write(file_content)


def convert_docx_to_pdf(docx_path: str, pdf_path: str):
    """
    将 Word 文档转换为 PDF
    使用 LibreOffice 命令行工具进行转换
    """
    try:
        # 使用 LibreOffice 转换 docx 到 pdf
        cmd = [
            'libreoffice',
            '--headless',
            '--convert-to', 'pdf',
            '--outdir', os.path.dirname(pdf_path),
            docx_path
        ]

        result = subprocess.run(cmd, capture_output=True, text=True, timeout=60)

        # LibreOffice 会在 outdir 下生成 以 docx 基名 命名的 PDF
        out_dir = os.path.dirname(pdf_path) or os.getcwd()
        expected_name = os.path.splitext(os.path.basename(docx_path))[0] + ".pdf"
        generated_pdf = os.path.join(out_dir, expected_name)

        if result.returncode == 0:
            # 若 LibreOffice 生成的文件存在，则按需重命名到目标路径
            if os.path.exists(pdf_path):
                logger.info(f"转换成功: {pdf_path}")
                return True
            if os.path.exists(generated_pdf):
                try:
                    if os.path.abspath(generated_pdf) != os.path.abspath(pdf_path):
                        os.replace(generated_pdf, pdf_path)
                    logger.info(f"转换成功: {pdf_path}")
                    return True
                except Exception as move_err:
                    logger.error(f"移动生成的PDF失败: {generated_pdf} -> {pdf_path}, error: {move_err}")
                    return False
            # 未找到生成文件
            logger.error(f"未找到生成的PDF: {generated_pdf}, stderr: {result.stderr}")
            return False
        else:
            logger.error(f"LibreOffice转换失败: {result.stderr}")
            return False

    except subprocess.TimeoutExpired:
        logger.error(f"转换超时: {docx_path}")
        return False
    except Exception as e:
        logger.error(f"转换异常: {e}")
        return False


def _get_extension_from_mime(mime_type: str) -> str:
    """从MIME类型获取文件扩展名"""
    mime_map = {
        # 文档类型
        'application/pdf': '.pdf',
        'application/msword': '.doc',
        'application/vnd.openxmlformats-officedocument.wordprocessingml.document': '.docx',
        'application/vnd.ms-excel': '.xls',
        'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': '.xlsx',
        'application/vnd.ms-powerpoint': '.ppt',
        'application/vnd.openxmlformats-officedocument.presentationml.presentation': '.pptx',
        'text/plain': '.txt',
        'text/html': '.html',
        'text/csv': '.csv',
        'application/rtf': '.rtf',
        'application/vnd.oasis.opendocument.text': '.odt',
        'application/vnd.oasis.opendocument.spreadsheet': '.ods',
        'application/vnd.oasis.opendocument.presentation': '.odp',
        
        # 压缩文件
        'application/zip': '.zip',
        'application/x-rar-compressed': '.rar',
        'application/x-7z-compressed': '.7z',
        'application/x-tar': '.tar',
        'application/gzip': '.gz',
        
        # 图片类型
        'image/jpeg': '.jpg',
        'image/png': '.png',
        'image/gif': '.gif',
        'image/bmp': '.bmp',
        'image/webp': '.webp',
        'image/svg+xml': '.svg',
        'image/tiff': '.tiff',
        
        # 音视频
        'video/mp4': '.mp4',
        'video/mpeg': '.mpeg',
        'video/x-msvideo': '.avi',
        'audio/mpeg': '.mp3',
        'audio/wav': '.wav',
        
        # 其他
        'application/json': '.json',
        'application/xml': '.xml',
        'application/octet-stream': '.bin',
    }
    return mime_map.get(mime_type.lower(), '')


def get_extension_from_url(url: str) -> Optional[str]:
    """
    从URL中提取文件扩展名
    
    Args:
        url: 文件URL
        
    Returns:
        扩展名（带点），如果无法提取则返回None
    """
    try:
        # 解析URL并解码
        parsed = urlparse(url)
        path = unquote(parsed.path)
        
        # 获取文件名
        filename = os.path.basename(path)
        
        # 提取扩展名
        _, ext = os.path.splitext(filename)
        
        # 确保扩展名有效（长度在2-5之间，只包含字母数字）
        if ext and 2 <= len(ext) <= 5 and ext[1:].replace('.', '').isalnum():
            return ext.lower()
        
        return None
    except Exception:
        return None


def get_extension_from_content_type(content_type: str) -> Optional[str]:
    """
    从Content-Type中推断文件扩展名
    
    Args:
        content_type: HTTP响应的Content-Type头
        
    Returns:
        扩展名（带点），如果无法推断则返回None
    """
    try:
        # 移除参数部分（如 charset=utf-8）
        mime_type = content_type.split(';')[0].strip().lower()
        
        if ext := _get_extension_from_mime(mime_type):
            return ext
        
        # 使用标准库推断
        ext = mimetypes.guess_extension(mime_type)
        if ext:
            # 某些特殊情况的修正
            if ext == '.jpe':
                return '.jpg'
            if ext == '.htm':
                return '.html'
            return ext
        
        return None
    except Exception:
        return None


def _is_image(filepath: str) -> bool:
    """检查文件是否为图片"""
    try:
        return imghdr.what(filepath) is not None
    except Exception:
        return False


def _get_image_extension(filepath: str) -> str:
    """获取图片的实际扩展名"""
    image_type = imghdr.what(filepath)
    if image_type:
        return f".{image_type}"
    return ''


def generate_md5_filename(file_content: bytes, filename: str = None, 
                         url: Optional[str] = None,
                         content_type: Optional[str] = None,
                         default_ext: str = '.bin') -> str:
    """
    生成基于MD5的唯一文件名
    
    格式：{md5_hash}{extension}
    例如：a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6.pdf
    
    Args:
        file_content: 文件内容（字节）
        filename: 原始文件名（用于提取扩展名）
        url: 文件URL（用于提取扩展名）
        content_type: HTTP Content-Type（用于推断扩展名）
        default_ext: 默认扩展名
        
    Returns:
        MD5哈希值 + 扩展名
    """
    # 计算文件内容的MD5
    md5_hash = hashlib.md5(file_content).hexdigest()
    
    # 获取扩展名
    ext = None
    
    # 1. 尝试从原始文件名提取扩展名
    if filename:
        _, file_ext = os.path.splitext(filename)
        if file_ext and 2 <= len(file_ext) <= 5 and file_ext[1:].replace('.', '').isalnum():
            ext = file_ext.lower()
    
    # 2. 如果文件名没有扩展名，尝试从URL提取
    if not ext and url:
        ext = get_extension_from_url(url)
    
    # 3. 如果URL没有扩展名，尝试从Content-Type推断
    if not ext and content_type:
        ext = get_extension_from_content_type(content_type)
    
    # 4. 使用默认扩展名
    if not ext:
        ext = default_ext
    
    return f"{md5_hash}{ext}"


def download_attachment(url: str, save_dir: str, max_retries: int = 3) -> Tuple[bool, str]:
    """下载附件并保存到指定目录，如果已经存在，则不重复下载
    output:
    {
        "status": True,
        "save_path": "/Users/wzq/Desktop/portAI/portai-server/document_loader/test_data/gb-file/1c8c2195de237dc6.jpg"
    }
    """
    try:
        if not url:
            logger.error("Image URL not provided")
            return False, ''

        # 处理base64数据
        if url.startswith('data:'):
            try:
                header, data = url.split(',', 1)
                mime_type = header.split(';')[0].split(':')[1]
                extension = _get_extension_from_mime(mime_type) or '.bin'
                img_name = f"{cal_md5(data)}{extension}"
                save_path = os.path.join(save_dir, img_name)

                if os.path.exists(save_path):
                    return True, save_path

                os.makedirs(save_dir, exist_ok=True)
                with open(save_path, "wb") as f:
                    f.write(base64.b64decode(data))
                return True, save_path
            except Exception as e:
                logger.error(f"Failed to decode base64 data: {str(e)}")
                return False, ''

        parsed_url = urlparse(url)
        if not all([parsed_url.scheme, parsed_url.netloc]):
            logger.error(f"Invalid URL: {url}")
            return False, ''

        filename = os.path.basename(parsed_url.path) or f"{cal_md5(url)}.tmp"
        save_path = os.path.join(save_dir, filename)
        if os.path.exists(save_path):
            return True, save_path

        for attempt in range(max_retries):
            try:
                response = requests.get(
                    url,
                    stream=True,
                    headers={'User-Agent': 'Mozilla/5.0'},
                    timeout=30
                )
                response.raise_for_status()

                os.makedirs(save_dir, exist_ok=True)

                img_bytes = bytearray()
                for chunk in response.iter_content(chunk_size=8192):
                    if chunk:
                        img_bytes.extend(chunk)

                if len(img_bytes) < 12 * 1024:
                    logger.warning("img bytes < 12 KB")
                    return False, ''

                with open(save_path, 'wb') as f:
                    f.write(img_bytes)

                # 如果是图片，检查并修正扩展名
                if _is_image(save_path):
                    actual_ext = _get_image_extension(save_path)
                    if actual_ext and not filename.lower().endswith(actual_ext):
                        new_path = os.path.splitext(save_path)[0] + actual_ext
                        os.rename(save_path, new_path)
                        save_path = new_path

                return True, save_path

            except requests.exceptions.RequestException as e:
                if attempt == max_retries - 1:
                    logger.error(f"Failed to download {url} after {max_retries} attempts: {str(e)}")
                    if os.path.exists(save_path):
                        try:
                            os.remove(save_path)
                        except OSError:
                            pass
                    return False, ''
                logger.warning(f"Download attempt {attempt + 1} failed: {str(e)}")

    except Exception as e:
        logger.error(f"Unexpected error downloading {url}: {str(e)}")
        return False, ''


def replace_img_src(text, token, nginx_prefix: str = '/api2', attachments: List = []) -> str:
    """
    替换文本中所有图片链接格式，并添加主备链接策略：
    从: ![](https://web-api.textin.com/ocr_image/external/002e250bcd0bd520.jpg)
    改为包含主备链接的格式

    支持多种格式：
    1. ![](https://.../abc123.jpg)
    2. <img src="https://.../abc123.jpg">
    3. ![alt text](https://.../abc123.jpg "title")
    """

    # 改进后的正则表达式，匹配更全面
    pattern = r'!\[([^\]]*)\]\(([^)]+)\)|<img[^>]+src=["\']([^"\']+)["\'][^>]*>'
    watermark_imgs = [i.origin_name for i in attachments if i.file_size < 12 * 1024]
    img_names = [i.origin_name for i in attachments]

    def replace_callback(match):
        # 处理 ![alt](url) 格式
        if match.group(2):
            original_url = match.group(2)
            img_name = original_url.split('/')[-1].split('?')[0]  # 去除URL参数
            if img_name in watermark_imgs or img_name not in img_names:
                return ''

            # 生成主备链接
            primary_url = f"{nginx_prefix}/image/{img_name}?token={token}"
            fallback_url = original_url

            # 返回包含主备链接的img标签
            # return f'<img src="{primary_url}" onerror="this.onerror=null; this.src=\'{fallback_url}\';" alt="{match.group(1) or "文档图片"}" style="max-width: 100%; height: auto;">'
            return f'<img src="{primary_url}" alt="{match.group(1) or "文档图片"}" style="max-width: 100%; height: auto;">'

        # 处理 <img src="url"> 格式
        elif match.group(3):
            original_url = match.group(3)
            img_name = original_url.split('/')[-1].split('?')[0]  # 去除URL参数
            if img_name in watermark_imgs or img_name not in img_names:
                return ''

            # 生成主备链接
            primary_url = f"{nginx_prefix}/image/{img_name}?token={token}"
            fallback_url = original_url

            # 替换原有img标签的src，并添加onerror处理
            original_tag = match.group(0)
            # 移除原有的onerror处理（如果有）
            original_tag = re.sub(r'\sonerror=[\'"][^\'"]*[\'"]', '', original_tag)
            # 替换src并添加onerror
            new_tag = original_tag.replace(
                f'src="{original_url}"',
                f'src="{primary_url}" onerror="this.onerror=null; this.src=\'{fallback_url}\'"'
            )
            # 确保有alt属性
            if 'alt=' not in new_tag:
                new_tag = new_tag.replace('src=', 'alt="文档图片" src=')

            return new_tag
        return match.group(0)

    return re.sub(pattern, replace_callback, text)


if __name__ == "__main__":
    import json
    from document_loader.pdf_utils import PdfCommonUtils

    with open("test.json", "r") as f:
        pdf_metadata = json.load(f)

    results = PdfCommonUtils.analyze_result(pdf_metadata)
    attachments = results.get("attachments", [])

    from utils.documents_utils import create_documents_from_pdf
    documents = create_documents_from_pdf(pdf_metadata, None, attachments)

    # 将 Document 序列化为可写入JSON的结构
    serializable = [{
        "page_content": d.page_content,
        "metadata": d.metadata,
    } for d in documents]
    with open("test_documents.json", "w") as f:
        json.dump(serializable, f, ensure_ascii=False, indent=2)

    # content = """
    # <table border=\"1\" ><tr>\n<td>编号</td>\n<td>图形标志</td>\n<td>名称</td>\n<td>设置范围和地点</td>\n</tr><tr>\n<td>1-04 </td>\n<td><img src=\"image_038\"></td>\n<td>当心腐蚀<br>Warning corrosive<br>substance </td>\n<td>有腐蚀性物质的作业地点</td>\n</tr><tr>\n<td>1-05 </td>\n<td><img src=\"image_039\"></td>\n<td>当心中毒<br>Warning toxic<br>material </td>\n<td>有剧毒品及有毒物质的生产、储运及使用场所</td>\n</tr><tr>\n<td>1-06 </td>\n<td><img src=\"image_040\"></td>\n<td>当心生物危害<br>Warning biological<br>hazard </td>\n<td>有生物危害的场所，如：医院传染病区，生物安全实验室，有害生物制剂生产、储运、使用等地点</td>\n</tr><tr>\n<td>1-07 </td>\n<td><img src=\"image_041\"></td>\n<td>当心触电<br>Warning electricity </td>\n<td>有发生触电危险的电器设备和线路，如：配电室、配电柜等</td>\n</tr><tr>\n<td>1-08 </td>\n<td><img src=\"image_042\"></td>\n<td>当心电缆<br>Warning cable </td>\n<td>有暴露的电缆或地面下有电缆施工的地点</td>\n</tr><tr>\n<td>1-09 </td>\n<td><img src=\"image_043\"></td>\n<td>当心自动启动<br>Warning automatic<br>start-up </td>\n<td>有自动启动装置的设备</td>\n</tr><tr>\n<td>1-10 </td>\n<td><img src=\"image_044\"></td>\n<td>当心机械伤人<br>Warning mechanical injury </td>\n<td>易发生机械卷入、轧压、碾压、剪切等机械伤害的作业地点</td>\n</tr><tr>\n<td>1-11 </td>\n<td><img src=\"image_045\"></td>\n<td>当心塌方<br>Warning collapse </td>\n<td>有塌方危险的地段、地区，如：堤坝及土方作业的深坑、深槽等</td>\n</tr><tr>\n<td>1-12 </td>\n<td><img src=\"image_046\"></td>\n<td>当心冒顶<br>Warning roof fall </td>\n<td>有冒顶危险的作业场所，如：矿井、隧道等</td>\n</tr><tr>\n<td>1-13 </td>\n<td><img src=\"image_047\"></td>\n<td>当心坑洞<br>Warning hole </td>\n<td>有坑洞易造成伤害的作业地点，如：构件的预留孔洞及各种深坑的上方等</td>\n</tr><tr>\n<td>1-14 </td>\n<td><img src=\"image_048\"></td>\n<td>当心落物<br>Warning falling<br>objects </td>\n<td>易发生落物危险的地点，如：高处作业、立体交叉作业的下方等</td>\n</tr><tr>\n<td>1-15 </td>\n<td><img src=\"image_049\"></td>\n<td>当心吊物<br>Warning overhead<br>load </td>\n<td>有吊装设备作业的场所，如：施工工地、仓库、车间等</td>\n</tr><tr>\n<td>1-16 </td>\n<td><img src=\"image_050\"></td>\n<td>当心碰头<br>Warning overhead<br>obstacle </td>\n<td>有发生碰头危险的部位或地点</td>\n</tr><tr>\n<td>1-17 </td>\n<td><img src=\"image_051\"></td>\n<td>当心挤压<br>Warning crushing </td>\n<td>有产生挤压的装置、设备或场所，如：自动门、电梯门、车站屏蔽门等</td>\n</tr><tr>\n<td>1-18 </td>\n<td><img src=\"image_052\"></td>\n<td>当心烫伤<br>Warning scald </td>\n<td>有热源易造成伤害的作业地点，如：冶炼、锻造、铸造、热处理车间等</td>\n</tr><tr>\n<td>1-19 </td>\n<td><img src=\"image_053\"></td>\n<td>当心伤手<br>Warning injuring<br>hand </td>\n<td>易造成手部伤害的作业地点，如：玻璃制品、木制加工、机械加工车间等</td>\n</tr><tr>\n<td>1-20 </td>\n<td><img src=\"image_054\"></td>\n<td>当心夹手<br>Warning crushing of hands </td>\n<td>有产生挤压的装置、设备或部位，如：自动门、电梯门、列车车门等</td>\n</tr><tr>\n<td>1-21 </td>\n<td><img src=\"image_055\"></td>\n<td>当心扎脚<br>Warning splinter </td>\n<td>易造成脚部伤害的作业地点，如：铸造车间、木工车间、施工工地及有尖角散料等处</td>\n</tr><tr>\n<td>1-22 </td>\n<td><img src=\"image_056\"></td>\n<td>当心有犬<br>Warning guard dog </td>\n<td>有犬类活动的场所</td>\n</tr><tr>\n<td>1-23 </td>\n<td><img src=\"image_057\"></td>\n<td>当心弧光<br>Warning arc </td>\n<td>有弧光造成眼部伤害的各种作业场所</td>\n</tr><tr>\n<td>1-24 </td>\n<td><img src=\"image_058\"></td>\n<td>当心高温表面<br>Warning hot surface </td>\n<td>有灼烫危害的部位或场所</td>\n</tr><tr>\n<td>1-25 </td>\n<td><img src=\"image_059\"></td>\n<td>当心低温<br>Warning low<br>temperature/freezingconditions </td>\n<td>易导致冻伤的场所，如：冷库、气化器表面、存在液化气体的场所等</td>\n</tr><tr>\n<td>1-26 </td>\n<td>U<br><img src=\"image_060\"></td>\n<td>当心磁场<br>Warning magnetic field </td>\n<td>有强磁场的区域或场所</td>\n</tr><tr>\n<td>1-27 </td>\n<td><img src=\"image_061\"></td>\n<td>当心电离辐射<br>Warning radioactive material/ionizing<br>radiation </td>\n<td>有产生电离辐射危害的作业场所，如：生产、储运、使用存在电离辐射物质的作业区</td>\n</tr><tr>\n<td>1-28 </td>\n<td><img src=\"image_062\"></td>\n<td>当心裂变物质<br>Warning fission matter </td>\n<td>有裂变物质的作业场所，如：裂变物质使用车间、储运仓库、容器等</td>\n</tr><tr>\n<td>1-29 </td>\n<td><img src=\"image_063\"></td>\n<td>当心激光<br>Warning laser beam </td>\n<td>有激光产品和生产、使用、维修激光产品的场所</td>\n</tr><tr>\n<td>1-30 </td>\n<td><img src=\"image_064\"></td>\n<td>当心非电离辐射<br>Warning non-ionizing radiation </td>\n<td>有非电离辐射的作业场所</td>\n</tr><tr>\n<td>1-31 </td>\n<td><img src=\"image_065\"></td>\n<td>当心叉车和其他<br>场（厂）内机动车辆<br>Warning forklift trucks and other industrial vehicles </td>\n<td>有叉车和其他场（厂）内机动车辆通行的场所</td>\n</tr><tr>\n<td>1-32 </td>\n<td><img src=\"image_066\"></td>\n<td>当心车辆<br>Warning vehicle </td>\n<td>厂内车、人混合行走的路段，道路的拐角处、平交路口；车辆出入较多的厂房、车库等出入口处</td>\n</tr><tr>\n<td>1-33 </td>\n<td><img src=\"image_067\"></td>\n<td>当心火车<br>Warning train </td>\n<td>厂内铁路与道路平交路口，厂（矿）内铁路运输线等</td>\n</tr><tr>\n<td>1-34 </td>\n<td><img src=\"image_068\"></td>\n<td>当心坠落<br>Warning dropping<br>down </td>\n<td>易发生坠落危害的作业地点</td>\n</tr><tr>\n<td>1-35 </td>\n<td><img src=\"image_069\"></td>\n<td>当心绊倒<br>Warning floor-level obstacle </td>\n<td>有障碍物，绊倒易造成伤害的地点</td>\n</tr><tr>\n<td>1-36 </td>\n<td><img src=\"image_070\"></td>\n<td>当心跌落<br>Warning drop (fall) </td>\n<td>有跌落危险的地点，如：无护栏的梯台、水边等</td>\n</tr><tr>\n<td>1-37 </td>\n<td><img src=\"image_071\"></td>\n<td>当心滑倒<br>Warning slippery<br>surface </td>\n<td>有滑跌危险的地点，如：地面有油、冰、水、青苔等位置</td>\n</tr><tr>\n<td>1-38 </td>\n<td><img src=\"image_072\"></td>\n<td>当心落水<br>Warning falling into water </td>\n<td>有落水危险的地点，如：城市河流，消防水池等</td>\n</tr><tr>\n<td>1-39 </td>\n<td><img src=\"image_073\"></td>\n<td>当心缝隙<br>Warning gap </td>\n<td>有缝隙的装置、设备或场所，如：自动门、电梯门、列车等</td>\n</tr></table>
    # """

    # 测试更通用的正则，兼容 image_*/img_*，以及任意属性顺序
    # res = []
    # res.extend(re.findall(r'<--\s*((?:image|img)_\w+)\s*-->', content))
    # res.extend(re.findall(r'<img[^>]+src=["\']((?:image|img)_\w+)["\']', content))
    # print(res)
